﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Diagnostics;
using DeployGames.TweenEngine.equations;

namespace DeployGames.TweenEngine
{
    public class Tween : BaseTween<Tween>
    {
        public static int INFINITY = -1;
        private static int combinedAttrsLimit = 3;
        private static int waypointsLimit = 0;

        private IBaseAcessor localTarget;
        private Type targetClass;
        private IBaseTweenAcessor accessor;
        private int type;
        private TweenEquation equation;
        private TweenPath path;

        // General
        private bool isFrom;
        private bool isRelative;
        private int combinedAttrsCnt;
        private int waypointsCnt;

        // Values
        private float[] startValues = new float[combinedAttrsLimit];
        private float[] targetValues = new float[combinedAttrsLimit];
        private float[] waypoints = new float[waypointsLimit * combinedAttrsLimit];

        // Buffers
        private float[] accessorBuffer = new float[combinedAttrsLimit];
        private float[] pathBuffer = new float[(2 + waypointsLimit) * combinedAttrsLimit];

        private static TweenPoolCallback poolCallback = new TweenPoolCallback();
        private static TweenPool pool = new TweenPool(10, poolCallback);

        private static Dictionary<Type, IBaseTweenAcessor> registeredAccessors =
            new Dictionary<Type, IBaseTweenAcessor>();

        public static void registerAccessor(Type someClass, IBaseTweenAcessor defaultAccessor)
        {
            registeredAccessors.Add(someClass, defaultAccessor);
        }

        public static void registerAccessor(Type someClass, TweenAccessor<IBaseAcessor> defaultAccessor)
        {
            registeredAccessors.Add(someClass, defaultAccessor);
        }

        public static IBaseTweenAcessor getRegisteredAccessor(Type someClass)
        {
            return registeredAccessors[someClass];
        }

        public static void setCombinedAttributesLimit(int limit)
        {
            Tween.combinedAttrsLimit = limit;
        }

        public static void setWaypointsLimit(int limit)
        {
            Tween.waypointsLimit = limit;
        }

        public static String getVersion()
        {
            return "6.3.3.Net";
        }

        public static int getPoolSize()
        {
            return pool.size();
        }

        public static void ensurePoolCapacity(int minCapacity)
        {
            pool.ensureCapacity(minCapacity);
        }

        public static Tween to(IBaseAcessor target, int tweenType, float duration)
        {
            Tween tween = pool.get();
            tween.setup(target, tweenType, duration);
            tween.ease(Quad.INOUT);
            tween.Path(TweenPaths.catmullRom);
            return tween;
        }

        public static Tween from(IBaseAcessor target, int tweenType, float duration)
        {
            Tween tween = pool.get();
            tween.setup(target, tweenType, duration);
            tween.ease(Quad.INOUT);
            tween.Path(TweenPaths.catmullRom);
            tween.isFrom = true;
            return tween;
        }

        public static Tween set(IBaseAcessor target, int tweenType)
        {
            Tween tween = pool.get();
            tween.setup(target, tweenType, 0);
            tween.ease(Quad.INOUT);
            return tween;
        }

        public static Tween call(TweenCallback callback)
        {
            Tween tween = pool.get();
            tween.setup(null, -1, 0);
            tween.setCallback(callback);
            tween.setCallbackTriggers(TweenCallback.START);
            return tween;
        }

        public static Tween mark()
        {
            Tween tween = pool.get();
            tween.setup(null, -1, 0);
            return tween;
        }

        public Tween()
        {
            reset();
        }

        public override void reset()
        {
            base.reset();
            localTarget = null;
            targetClass = null;
            accessor = null;
            type = -1;
            equation = null;
            path = null;

            isFrom = isRelative = false;
            combinedAttrsCnt = waypointsCnt = 0;

            if (accessorBuffer.Length != combinedAttrsLimit)
            {
                accessorBuffer = new float[combinedAttrsLimit];
            }

            if (pathBuffer.Length != (2 + waypointsLimit) * combinedAttrsLimit)
            {
                pathBuffer = new float[(2 + waypointsLimit) * combinedAttrsLimit];
            }
        }

        private void setup(IBaseAcessor target, int tweenType, float duration)
        {
            if (duration < 0) throw new Exception("Duration can't be negative");

            this.localTarget = target;
            this.targetClass = target != null ? findTargetClass() : null;
            this.type = tweenType;
            this.duration = duration;
        }

        private Type findTargetClass()
        {
            if (registeredAccessors.ContainsKey(localTarget.GetType())) return localTarget.GetType();
            if (localTarget.GetType() == typeof(IBaseTweenAcessor)) return localTarget.GetType();
            if (registeredAccessors.ContainsKey(localTarget.GetType().BaseType)) return localTarget.GetType();
            return null;
        }

        public Tween cast(Type targetClass)
        {
            if (IsStarted()) throw new Exception("You can't cast the target of a tween once it is started");
            this.targetClass = targetClass;
            return this;
        }

        public Tween target(float targetValue)
        {
            targetValues[0] = targetValue;
            return this;
        }

        public Tween target(float targetValue1, float targetValue2)
        {
            targetValues[0] = targetValue1;
            targetValues[1] = targetValue2;
            return this;
        }

        public Tween target(float targetValue1, float targetValue2, float targetValue3)
        {
            targetValues[0] = targetValue1;
            targetValues[1] = targetValue2;
            targetValues[2] = targetValue3;
            return this;
        }

        public Tween target(params float[] targetValues)
        {
            if (targetValues.Length > combinedAttrsLimit) throwCombinedAttrsLimitReached();
            Array.Copy(targetValues, 0, this.targetValues, 0, targetValues.Length);
            return this;
        }

        public Tween targetRelative(float targetValue)
        {
            isRelative = true;
            targetValues[0] = IsInitialized() ? targetValue + startValues[0] : targetValue;
            return this;
        }

        public Tween targetRelative(float targetValue1, float targetValue2)
        {
            isRelative = true;
            targetValues[0] = IsInitialized() ? targetValue1 + startValues[0] : targetValue1;
            targetValues[1] = IsInitialized() ? targetValue2 + startValues[1] : targetValue2;
            return this;
        }

        public Tween targetRelative(float targetValue1, float targetValue2, float targetValue3)
        {
            isRelative = true;
            targetValues[0] = IsInitialized() ? targetValue1 + startValues[0] : targetValue1;
            targetValues[1] = IsInitialized() ? targetValue2 + startValues[1] : targetValue2;
            targetValues[2] = IsInitialized() ? targetValue3 + startValues[2] : targetValue3;
            return this;
        }

        public Tween targetRelative(params float[] targetValues)
        {
            if (targetValues.Length > combinedAttrsLimit) throwCombinedAttrsLimitReached();
            for (int i = 0; i < targetValues.Length; i++)
            {
                this.targetValues[i] = IsInitialized() ? targetValues[i] + startValues[i] : targetValues[i];
            }

            isRelative = true;
            return this;
        }

        public Tween waypoint(float targetValue)
        {
            if (waypointsCnt == waypointsLimit) throwWaypointsLimitReached();
            waypoints[waypointsCnt] = targetValue;
            waypointsCnt += 1;
            return this;
        }

        public Tween waypoint(float targetValue1, float targetValue2)
        {
            if (waypointsCnt == waypointsLimit) throwWaypointsLimitReached();
            waypoints[waypointsCnt * 2] = targetValue1;
            waypoints[waypointsCnt * 2 + 1] = targetValue2;
            waypointsCnt += 1;
            return this;
        }

        public Tween waypoint(float targetValue1, float targetValue2, float targetValue3)
        {
            if (waypointsCnt == waypointsLimit) throwWaypointsLimitReached();
            waypoints[waypointsCnt * 3] = targetValue1;
            waypoints[waypointsCnt * 3 + 1] = targetValue2;
            waypoints[waypointsCnt * 3 + 2] = targetValue3;
            waypointsCnt += 1;
            return this;
        }

        public Tween waypoint(params float[] targetValues)
        {
            if (waypointsCnt == waypointsLimit) throwWaypointsLimitReached();
            Array.Copy(targetValues, 0, waypoints, waypointsCnt * targetValues.Length, targetValues.Length);
            waypointsCnt += 1;
            return this;
        }

        public Tween Path(TweenPath path)
        {
            this.path = path;
            return this;
        }

        public Object getTarget()
        {
            return localTarget;
        }

        public int getType()
        {
            return type;
        }

        public TweenEquation getEasing()
        {
            return equation;
        }

        public Tween ease(TweenEquation easeEquation)
        {
            this.equation = easeEquation;
            return this;
        }

        public float[] getTargetValues()
        {
            return targetValues;
        }

        public int getCombinedAttributesCount()
        {
            return combinedAttrsCnt;
        }

        public IBaseTweenAcessor getAccessor()
        {
            return accessor;
        }

        public Type getTargetClass()
        {
            return targetClass;
        }

        public override Tween build()
        {
            if (localTarget == null) return this;
            accessor = null;
            if (registeredAccessors.ContainsKey(targetClass))
            {
                accessor = registeredAccessors[targetClass];
            }
            if (accessor == null && localTarget.GetType() == typeof(TweenAccessor<IBaseAcessor>))
            {
                accessor = (localTarget as TweenAccessor<IBaseAcessor>);
            }
            if (accessor != null) combinedAttrsCnt = accessor.getValues(localTarget, type, accessorBuffer);
		    else throw new Exception("No TweenAccessor was found for the target");
		    if (combinedAttrsCnt > combinedAttrsLimit) throwCombinedAttrsLimitReached();
		    return this;
        }

        public override void free()
        {
            pool.free(this);
        }

        protected override void initializeOverride()
        {
            if (localTarget == null) return;
            accessor.getValues(localTarget, type, startValues);
            for (int i = 0; i < combinedAttrsCnt; i++)
            {
                targetValues[i] += isRelative ? startValues[i] : 0;
                for (int ii = 0; ii < waypointsCnt; ii++)
                {
                    waypoints[ii * combinedAttrsCnt + i] += isRelative ? startValues[i] : 0;
                }
                if (isFrom)
                {
                    float tmp = startValues[i];
                    startValues[i] = targetValues[i];
                    targetValues[i] = tmp;
                }
            }
        }

        protected override void updateOverride(int step, int lastStep, bool isIterationStep, float delta)
        {
            if (localTarget == null || equation == null) return;

		// Case iteration end has been reached

		if (!isIterationStep && step > lastStep) {
            accessor.setValues(localTarget, type, isReverse(lastStep) ? startValues : targetValues);
			return;
		}

		if (!isIterationStep && step < lastStep) {
            accessor.setValues(localTarget, type, isReverse(lastStep) ? targetValues : startValues);
			return;
		}

		// Validation
		Debug.Assert(isIterationStep);
		Debug.Assert(getCurrentTime() >= 0);
		Debug.Assert(getCurrentTime() <= duration);

		// Case duration equals zero

		if (duration < 0.00000000001f && delta > -0.00000000001f) {
            accessor.setValues(localTarget, type, isReverse(step) ? targetValues : startValues);
			return;
		}

		if (duration < 0.00000000001f && delta < 0.00000000001f) {
            accessor.setValues(localTarget, type, isReverse(step) ? startValues : targetValues);
			return;
		}

		// Normal behavior

		float time = isReverse(step) ? duration - getCurrentTime() : getCurrentTime();
		float t = equation.compute(time/duration);

		if (waypointsCnt == 0 || path == null) {
			for (int i=0; i<combinedAttrsCnt; i++) {
				accessorBuffer[i] = startValues[i] + t * (targetValues[i] - startValues[i]);
			}

		} else {
			for (int i=0; i<combinedAttrsCnt; i++) {
				pathBuffer[0] = startValues[i];
				pathBuffer[1+waypointsCnt] = targetValues[i];
				for (int ii=0; ii<waypointsCnt; ii++) {
					pathBuffer[ii+1] = waypoints[ii*combinedAttrsCnt+i];
				}

				accessorBuffer[i] = path.compute(t, pathBuffer, waypointsCnt+2);
			}
		}

        accessor.setValues(localTarget, type, accessorBuffer);
        }

        protected override void forceStartValues()
        {
            if (localTarget == null) return;
            accessor.setValues(localTarget, type, startValues);
        }

        protected override void forceEndValues()
        {
            if (localTarget == null) return;
            accessor.setValues(localTarget, type, targetValues);
        }

        protected override bool containsTarget(object target)
        {
            return this.localTarget == target;
        }

        protected override bool containsTarget(object target, int tweenType)
        {
            return this.localTarget == target && this.type == tweenType;
        }

        private void throwCombinedAttrsLimitReached()
        {
            String msg = "You cannot combine more than " + combinedAttrsLimit + " "
                + "attributes in a tween. You can raise this limit with "
                + "Tween.setCombinedAttributesLimit(), which should be called once "
                + "in application initialization code.";
            throw new Exception(msg);
        }

        private void throwWaypointsLimitReached()
        {
            String msg = "You cannot add more than " + waypointsLimit + " "
                + "waypoints to a tween. You can raise this limit with "
                + "Tween.setWaypointsLimit(), which should be called once in "
                + "application initialization code.";
            throw new Exception(msg);
        }
    }
}
